/**
* @brief 包装了基于ASCII可见字符的数据传输协议 
* @description
* #### 基本原理 
* 本协议基于ASCII可见字符，以特定的格式传输数据。协议功能有限，以满足项目
* 需求为主。协议代码仅涉及字符串的处理，从而与通信的硬件隔离开来，也就是
* 说，无论硬件采用串口、IIC还是SPI，只要能传输ASCII字符，并向指定的接口函数
* 提供数据，该协议就能正常工作。同时，由于C代码涉及到字符串操作，因此要求目
* 标平台支持 C 标准库中字符串相关的部分。 
* 
* 本协议以数据帧为单位传输数据。一个数据帧在内容上由标签和数值构成，此外还
* 有用于标确定边界的分隔字符。数据帧的具体格式如下：
```
   帧开始字符 标签 标签分隔字符 数值1 数值分隔字符 数值2 ... 帧结束字符
   SYM_START label SEP_LABEL val1 SEP_VALUE val2 ... SYM_END
   
   例如:
   <color: 23, 69, 77>
   <pos: 50.34, 45.39>
```
*
* 一个数据帧只能有一个标签，可以有0个或多个数值。标签用于说明数值的用途，
* 合起来看，标签就像是函数名，数值就好像是函数参数，因此建议标签遵循 
* 变量命名规则。仅含标签，没有数值的数据帧就像是一条指令，要求接收方执行
* 某些操作，而带有数值的数据帧则相当于指令带上了参数。 
*
* 在C语言实现中，标签储存为字符串，数值全部储存在一个 float
* 数组当中。标签字符串与float数组的容量可通过修改头文件中的定义来适应项目
* 需求。各分隔字符也可以通过头文件中的相关定义来修改，只要数据中不会出现
* 这些分隔符即可（虽然想不出有什么理由要改）。
*  
* 在当前的 C语言实现下，经过测试，以下数据帧是合法的：
```
    // 有效数据帧 
    <tomas: 22,33>       // 标准情况 
    <  tomas : 22, 33 >  // 标签中含空格 
    < tt >               // 不带数值的数据帧 
    < xx : >             // 不带数值，但有标签分隔符的数据帧 
    <tomas 22, 33>       // 缺少标签分隔符的数据帧,整体被视为一个label字符串 
```
* 在当前的C语言实现下，有以下几点需要注意：
* - 当前实现不会去除标签字符串首尾的空格，因此发送方要注意不要引入额外的空格，
*   如第2、3、4行所示。否则接收方在后续的字符串比较中可能会遇到问题。
* - 对于不带数值的数据帧，在当前的实现下，<tt> 和 <tt:>都被视为正确的。但是
*   不建议采用如后者的格式，因为标签分隔符表达了期望后面有数值，而事实上却
*   没有数值，存在一定程度上的误解。另一方面，<tt> 这样的格式，看起来更像是
*   指令。 
* - 如上第5条所示，数据帧中缺失了标签分隔符，于是除首尾分隔符外的整个字符串
*   都被视为标签。这可以被认为是个bug，因为当前的实现只是根据相应分隔符提取
*   对应元素，而不对内容做过多的检查。
* - 在communication.c中，申请了一个字符数组用作缓存。这个数组是私有的，其他
*   文件不能访问，如果需要改变该缓存的容量，请修改源文件中的相关宏。 
*
* 经过测试，以下的数据帧被视为是无效的： 
```
    // 无效数据帧 
    tomas: 22, 33>,  // 缺失帧开始字符 
    <tomas: 22, 33,  // 缺失帧结束字符 
``` 
* 
* #### 使用方法
* 可运行的例子请参考 demo.c 
* 
* C语言实现对外提供的接口主要如下，简洁起见，未写出形参，其他接口
* 详见communication.h。 
```
    status_t 枚举类型; // 用于函数反馈信息 
    struct DataFrame;  // 从一个数据帧中解析出的数据 
    
    void DF_init();             // 初始化 DataFrame 结构体
    void init_communication()   // 初始化模块 
    status_t read_data()        // 接收数据 
    status_t pack_data()        // 输出数据 
```
* 一个DataFrame结构体变量对应于一个数据帧。应用程序可通过字符串 DataFrame.label
* 和 float 数组 DataFrame.values获取数据帧中的值。
* 
* 一般串口等硬件是按字节接收数据的。对于每个新接收的字符，都要传入read_data()，
* 该函数的返回值指明当前是否接收到完整合法的数据帧。假设每次调用usart_read()
* 函数可通过串口读取到一个字符，使用本协议大致的例子如下:
```
    init_communication();   // 首先初始化模块
    struct DataFrame data;  // 声明一个数据帧结构体 
    DF_init(&data);         // 初始化结构体
    
    while (1)
    {
        char ch = usart_read();  // 假设usart_read()返回一个字符
        status_t ret = read_data(ch, &data);  // 传入刚刚读取到的字符
        
        // 当read_data()的返回值为枚举值OK时，说明模块已接收到完整的数据帧
        // 并成功解析，解析结果通过结构体 data 传出。若read_data()的返回值
        // 为其他值时，说明数据帧的接收尚未完成，data 中为何值是不确定的。 
        if (ret == OK)
        {
            printf("lb:%s, val1:%f, val2:%f", data.label, data.values[0], data.values[1]);
        }
    }
``` 
* 
* #### 实现原理
*  基本思想是将通信协议实现为一个模块，跨接在底层ASCII通信和应用层之间。
*  将一个数据帧视为一个"对象"，映射到结构体中，应用层使用结构体的数据，
*  而底层则通过字符串进行传输。
*
*  对于接收，主要经以下三个步骤：
*      1. detector，通过定义的格式检测数据帧的完整性。
*      2. collector，根据detector反馈的结果，将数据加入缓存，或清空缓存。
*      3. parser，根据detector的结果，当接收到完整数据帧时，则解析数据帧字符串。
*  以上叙述中用的是名词，而实际上对应的函数使用的是动词。以上三步属于模块内部的
*  细节，因此相关函数与变量均用static隐藏起来。将以上三步封装在read_data()函数
*  中，作为提供给应用层的接口。 
*  
*  对于发送，则比较简单，根据结构体构造符合格式要求的字符串即可。对此提供
*  pack_data()函数，以便构造符合要求的字符串。 
* 
* #### 一些妥协
* - 考虑到实现上的难度，目前接收到的数据只能以字符的形式一个一个传进来，而不能
*   直接传入字符串。考虑到串口等通信一般是每个字节触发一次中断，因此这种做法是
*   可以接收的。也因为此，数据帧的定界符也都是单字节，否则要考虑到非常多的特殊
*   情况，十分麻烦。 
* 
*  - 本人对C语言的字符串操作不大熟练，因此有些在可读性上可能会有些缺点。有些测
*    试也不全面，可能会出现很多bug。不过对于涉及数组的操作，已经尽力在通过函数
*    来避免越界了。 
*
* @author Stone at BlueNet，GDOU
* @date Oct. 2020 
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "communication.h"


#define FRAME_BUFFER_SIZE 50

// ===== ===== ===== ===== =====
// FrameBuffer ADT
// ===== ===== ===== ===== =====
// Attention：为避免越界，修改数组的操作请通过相关函数完成（函数内置边界检测） 
static struct FrameBuffer
{
    char buffer[FRAME_BUFFER_SIZE];  // 数据帧接收缓存 
    int len;					     // 当缓存中已被数据占用的长度 
    int size;                        // 缓存的容量，不包括末尾的空字符串 
    int is_start_detected;           // 用于记录是否检测到帧开始字符 
} g_recv;


/**
* @breif 初始化FrameBuffer，将各成员设置为正确的值 
* @param buf[out] 待初始化的缓存结构体 
*/
static void init_buffer(struct FrameBuffer *buf)
{
    memset(buf->buffer, 0, FRAME_BUFFER_SIZE);
    buf->len = 0;
    buf->size = FRAME_BUFFER_SIZE - 1;  // 预留字符串末尾空字符串的位置 
    buf->is_start_detected = 0;
}


/**
* @breif 将缓存清空。
* @param buf[out] 待初始化的缓存结构体  
*/
static void empty_buffer(struct FrameBuffer *buf)
{
	if (buf->len < buf->size) 
	{
	    memset(buf->buffer, 0, buf->len);
    }
    else
    {
        memset(buf->buffer, 0, buf->size);
    }
    
    buf->len = 0;
}


/**
* @breif 将收到的字符添加到 buffer 中。 
* @param ch[in] 将要存入 buffer 的字符
* @param buf[out] 被存入的 buffer 
* @return 是否存入成功。成功返回``OK``，若失败（数组越界）则返回``ERR``。 
*/
static status_t push_buffer(char ch, struct FrameBuffer *buf)
{
	if ((buf->len + 1) < buf->size)
	{
		buf->buffer[buf->len] = ch;
		buf->len += 1;
		return OK;
	}
	return ERR;
}


// ===== ===== ===== ===== =====
// DataFrame ADT
// ===== ===== ===== ===== =====
/**
* @brief 初始化DataFrame结构体，使其处于正确的初始状态
* @param data[out] 待初始化的结构体 
*/
void DF_init(struct DataFrame *data)
{
    memset(data->label, 0, MAX_LABEL_LEN);
    memset(data->values, 0, MAX_N_VALUES*sizeof(float));
    data->n_values = 0;
}


/**
* @brief 设置结构体label成员的值,实际就是memcpy 
* @param str[in] 待复制label的字符串 
* @param len[in] 复制多少个字节,相当于memcpy的参数n 
* @param data[out] 要被设置的结构体
* @return 设置成功返回OK,若返回错误,可能是字符串的长度过长. 
*/ 
status_t DF_set_label(const char *str, int len, struct DataFrame *data)
{
    if (len < (MAX_LABEL_LEN - 1))
    {
        memcpy(data->label, str, len);
        
        return OK;
    }
    return ERR;
}


/**
*@brief 将数值从尾部添加进数据帧结构体的float数组中
*@param val[in] 待存入的值
*@param data[out] 被存入值的结构体 
*@return 若添加成功则返回OK,若数组已满则返回ERR 
*/ 
status_t DF_push_value(float val, struct DataFrame *data)
{
    if (data->n_values < MAX_N_VALUES)
    {
        data->values[data->n_values] = val;
        data->n_values += 1;
        
        return OK;
    }
    
    return ERR;
}


// ===== ===== ===== ===== =====
// Functions dealing with Protocol
// ===== ===== ===== ===== =====
/**
* @brief 检测边界字符.
* -假设处于理想情况，不考虑有数据丢失的情况。 
* -注意：对于接收到的每个字符，都只能使用一次本函数，否则会导致错误的结果。
* -例如，接收到了字符 'a'，但使用了两次本函数，结果就相当于接收到了'aa'。 
* @param ch[in] 接收到的新字符。
* @param buf[out] 接收字符的缓存。
* @return ``OK``:已收到完整的数据帧，可以开始解析，``ERR``:数据帧尚不完整, 
*    ``ERR_START_DETECTED``: 遇到帧开始字符。 
*/
static status_t detect(char ch, struct FrameBuffer *buf)
{
    
    // 检测开始/结束符号 
	if ((buf->is_start_detected) && (ch == SYM_END))
	{
		buf->is_start_detected = 0;  // reset
		return OK;
	}
	else if (ch == SYM_START)
	{
		buf->is_start_detected = 1;	 // set
		return START_DETECTED;
	}
	return ERR;
}


/**
*@brief 在帧缓存字符串中寻找标签子字符串 
*@param str[in] 帧缓存字符串
*@param p_start[out] 指向标签的第一个字符的地址 
*@param len[out] 标签子字符串的长度 
*@return 若成功提取出标签子字符串,则返回OK,若未发现标签分隔符(意味着该数据帧
*    不含数值),则返回 SEP_LABEL_NOT_FOUND. 
*/ 
static status_t search_label(const char *str, char **p_start, int *len)
{
    status_t ret = OK;  // 返回值
    
    // 寻找标签结束的位置 
    char *p_end = strrchr(str, SEP_LABEL);  // supposed format: <lb: val>
    if (p_end == NULL)
    {
       ret = SEP_LABEL_NOT_FOUND;
       p_end = strrchr(str, SYM_END);    // supposed format: <lb>
    }
    
    if (p_end == NULL)
    {
        // 既没有找到``SEP_LABEL``，也没有找到``SYM_END``，认为解析出错 
        return ERR_PARSE;   
    }
    
    // 寻找标签起始的位置 
    *p_start = strchr(str, SYM_START);
    
    if (*p_start == NULL)
    {
        return ERR_PARSE;
    }
    
    // 设置输出值 
    (*p_start)++;  // 指针后移一位，指向标签文本起始的位置 
    *len = p_end - *p_start;
    
    return ret;
}


static status_t search_value(char *str, char **p_start)
{
    char sep[2] = {SEP_VALUE, 0};
    *p_start = strtok(str, sep);
    if (*p_start == NULL)
    {
        return ERR;
    }
}


static status_t parse_values(char *str, struct DataFrame *data) 
{
    char sep[2] = {SEP_VALUE, 0};
    char *p1 = strtok(str, sep);  // 指向子串的第一个字符
    char *p2 = NULL;              // 作为参数传给需要的函数
    float value;
    
    for (int i=0; i<MAX_N_VALUES; i++)
    {
        if (p1==NULL)  // 没有更多子串了 
        {
            return OK;
        }
        
        value = atof(p1);   // 将子串转换为小数 
        DF_push_value(value, data);  // 将转换后的数值推入data 
        
        p1 = strtok(NULL, sep);      // 查找下一个子串 
    }
    
    return ERR;  // 字符串中数值的数量太多，大于结构体可容纳的数量 
}


/**
* TODO 安全起见，未来最好还是将parse改成不需要修改原字符串的版本。 
* @breif 解析数据帧字符串 
* - 定界符的搜索使用了标准库函数 strtok()，但是该函数会修改原字符串，因此
* - 不能传入字符串常量，即 char *str = "abd" 一类变量，否则会引起bug。 
* @param recv[in] 数据帧字符串
* @param data[out] 数据帧结构体 
* @return 如果解析成功则返回OK 或 SEP_LABEL_NOT_FOUND, 否则返回其他值. 
*/
status_t parse(char *recv, struct DataFrame *data)
{
    char *p1 = NULL;  // 指向子串的第一个字符 
    int len = 0;      // 子串的长度 
    char *p2 = NULL;  // 做其他用途（主要作为参数传给需要的函数） 
//    char sep[2] = {0};
    float value = 0;  // 解析出的数值
    status_t ret_lb, ret_val;  // 函数的返回值 
    
//    printf("parse: %s\n", recv);  // debug
    // ===== ===== ===== ===== =====
    // extact label
    ret_lb = search_label(recv, &p1, &len);
    if ((ret_lb != OK) && (ret_lb != SEP_LABEL_NOT_FOUND))
    {
        return ERR_PARSE;
    }
    DF_set_label(p1, len, data);  // 复制标签内容到结构体 
    
    if (ret_lb == SEP_LABEL_NOT_FOUND) // 帧缓存中没有数值，故不必进行后续解析 
    {
        return OK;
    }
    // ===== ===== ===== ===== =====
    // extract values
    p1 += len;  // 指向``SEP_LABEL``的位置 (跳过label文本)
    p1++;       // 指向``SEP_LABEL``后一个位置 
//    sep[0] = SEP_VALUE;  // 替换为数据分隔字符
    
    ret_val = parse_values(p1, data);
    if (ret_lb == OK)  // 宽容<lb:>，有``SYM_LABEL``无数值的情况 
    {
        return OK;
    }
    
    return ret_val;
}


/**
* @brief 求出一个数的整数部分有多少位,用于构造字符串时确定数字的长度. 
*  将待求数分别除以1,10,100,直到商为0,便得整数部分的位数. 对于浮点数,
*  为提高运算效率,先转为整型再计算, 应注意浮点数的值不要超过整型的范围. 
* @param num [in] 待求的数
* @return 求出的位数 
*/
static int how_many_digits(int num)
{
    int n = 1;        // 位数 
    int divider = 1;  // 从个位开始
    
    if (num == 0)
    {
        return 1;  // 0占一位 
    }
    
    while (num / divider)
    {
        n++;
        divider *= 10;
    }
    
    return n-1;
} 


// ===== ===== ===== ===== =====
// External Interface 
// ===== ===== ===== ===== =====
/**
* @brief 初始化通信协议模块 
*/
void init_communication()
{
    init_buffer(&g_recv);
}


/**
* @breif 应用接口,读取,缓存,以及解析接收到的字符 
* @param ch[in] 接收到的字符 
* @param data[out] 从缓存字符串中解析出的数据
* @return 若返回 ``OK``, 表示读取到完整的数据帧并成功解析,此时结构体``data``
*    携带有解析出的数据.当返回值为其他时,表示数据尚不完整,此时不应使用data内的值. 
*/
status_t read_data(char ch, struct DataFrame *data) 
{
    status_t ret = detect(ch, &g_recv);
	switch (ret){
	    case OK:
	        push_buffer(ch, &g_recv);          // 将最后一个字符存入缓存 
	        DF_init(data);                     // 修改前先清空内容 
            ret = parse(g_recv.buffer, data);  // 解析数据帧 
	        empty_buffer(&g_recv);             // 清空缓存 
	        break;
        case ERR:
            push_buffer(ch, &g_recv);
            break;
        case START_DETECTED:
            empty_buffer(&g_recv);
            push_buffer(ch, &g_recv);
            break;
    }

    return ret;
}


/**
* @brief 将数据帧结构体构造成数据帧字符串 
*  本函数不检查数组是否越界,调用者应注意提前预估数据帧字符串的最大长度, 
*  以提供容量充足的字符数组. (主要是要检查长度的话,代码又长,又耗算力,不如
*  提前预估,给够字符串容量,简单暴力). 
* @param data [in] 提供数据的数据帧结构体
* @param outStr [out] 输出的数据帧字符串
* @return 若数据帧字符串构造成功,返回``OK`` 
*/
status_t pack_data(const struct DataFrame *data, char * const outStr)
{
	char format[10] = "";          // 数据帧中,数值的格式字符串 
	unsigned long len = 0;        // 输出字符串长度
    
	// 组装帧开始字符和标签 
	outStr[0] = SYM_START;
	strcpy(&(outStr[1]), data->label);
	len = 1 + strlen(data->label);
	
    outStr[len] = SEP_LABEL;
	len++;
	
	// 构造格式字符串 
	sprintf(format, "%%d%c", SEP_VALUE);  // 生成格式字符串
    //puts(format);  // debug
    
    // 组装数值部分
	for (int i=0; i < (data->n_values); i++)
	{
	    sprintf(&(outStr[len]), format, (int)(data->values)[i]);  // 把格式字符串追加到数据帧中 
	    len += strlen(&(outStr[len]));
    }
    
    // 把尾部的 数值分隔符 替换为 帧结束字符
    outStr[len -1] = SYM_END;
    
    return OK;
}
